Skip to content

feat: add Streamable HTTP MCP server#3

Merged
lafronzt merged 12 commits into
mainfrom
claude/relaxed-poincare-032335
May 24, 2026
Merged

feat: add Streamable HTTP MCP server#3
lafronzt merged 12 commits into
mainfrom
claude/relaxed-poincare-032335

Conversation

@lafronzt
Copy link
Copy Markdown
Owner

Summary

  • Adds src/mcp/http-server.ts — a second MCP entrypoint using StreamableHTTPServerTransport from @modelcontextprotocol/sdk
  • Mounts POST /mcp, GET /mcp, and DELETE /mcp using Node's built-in node:http (no new framework dependency)
  • Hard auth guard: all requests require an x-api-key header matching MCP_API_KEY; if the env var is unset, every request is rejected with 401
  • Per-session McpServer + StreamableHTTPServerTransport stored in a Map<string, Session>, keyed by the SDK-generated session ID, cleaned up via transport.onclose
  • Reuses registerTools and registerResources — zero duplication of tool/resource logic
  • isMain guard (process.argv[1] === fileURLToPath(import.meta.url)) prevents the server and PrismaClient from starting when the module is imported by tests
  • New package scripts: mcp:http:dev (tsx, no build) and mcp:http:start (compiled output at dist/src/mcp/http-server.js)
  • README documents environment variables, run commands, TLS proxy note, and client configuration JSON block
  • 6 unit tests for the isAuthorized helper covering: unset key, empty key, missing header, wrong header, exact match, and array header value

What's unchanged

The stdio MCP server (src/mcp/server.ts) and the Fastify HTTP API (src/server.ts, src/app.ts, src/http/routes.ts) are untouched.

Test plan

  • npm run lint passes (TypeScript strict, no errors)
  • npm run build passes and produces dist/src/mcp/http-server.js
  • npm test passes — 35/35 tests across 3 files, no regressions
  • Smoke test: MCP_API_KEY=secret npm run mcp:http:dev starts on port 3001
  • Unauthenticated request: curl -s http://localhost:3001/mcp returns {"error":"Missing or invalid API key"} with HTTP 401

🤖 Generated with Claude Code

lafronzt and others added 5 commits May 24, 2026 12:34
Adds a read-only Model Context Protocol server so AI clients can
discover and query engineering standards from the same PostgreSQL
database used by the Fastify HTTP API.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- toolError now serializes AppError code and details into JSON so MCP
  clients get structured error metadata, not just the message string
- get_standard validates rule_key against the same regex as the HTTP API
  to fail fast on malformed keys before touching the database
- list_standards uses z.coerce.number() for limit/offset so LLM-generated
  string values round-trip correctly
- Remove redundant per-type catch branches in get_standard and
  applicable_standards — both paths returned toolError(err) identically
- Add test/mcp-tools.test.ts covering all four tools, changed_paths
  array mapping, empty-result cases, and AppError serialization

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- toolError now includes statusCode alongside code and message so MCP
  clients can distinguish error types (400/404/409) without mapping codes
- changed_paths uses .min(1) to reject empty arrays, aligning with the
  HTTP API's "omit instead of empty" contract
- SIGINT/SIGTERM handlers now catch shutdown errors and exit non-zero
  instead of silently swallowing Prisma disconnect failures
- Refactor tool handlers into createHandlers(service) so tests call them
  directly without reaching into SDK private fields; McpServer and
  _registeredTools are no longer referenced in tests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Non-AppError exceptions now return a structured JSON internal_error
  payload and log to stderr, preventing internal detail leakage
- listStandards defaults status to "active" in the handler itself so
  the service receives an explicit filter and applies latest-by-ruleKey
  deduplication correctly when called with no arguments
- applicableStandards defaults its argument to {} so invoking the
  handler with no args does not throw on destructuring
- Test updated to call listStandards({}) and assert all returned rules
  are active, actually exercising the default-status contract

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds src/mcp/http-server.ts — a second MCP entrypoint using
StreamableHTTPServerTransport that exposes the same four tools and two
resources as the existing stdio server, without touching either the stdio
server or the Fastify HTTP API.

Key details:
- Uses Node's built-in node:http (no new framework dependency)
- Mounts POST/GET/DELETE /mcp per the Streamable HTTP spec
- Hard auth guard via x-api-key header (MCP_API_KEY env var required;
  unset → all requests rejected 401)
- Per-session McpServer + transport stored in a Map, cleaned up on close
- isMain guard prevents server startup when the file is imported in tests
- New scripts: mcp:http:dev (tsx) and mcp:http:start (compiled output)
- README documents env vars, run commands, and client config block
- 6 unit tests for the isAuthorized helper

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 24, 2026 17:25
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds Model Context Protocol (MCP) support to the standards API by introducing MCP tool/resource registrations plus two runnable MCP server entrypoints: a stdio server and a Streamable HTTP server secured via an API key.

Changes:

  • Adds MCP tool handlers and registers four tools (list_standards, get_standard, latest_standards, applicable_standards) plus two resources (standards://latest, standards://rule/{rule_key}).
  • Introduces two MCP entrypoints: stdio (src/mcp/server.ts) and Streamable HTTP (src/mcp/http-server.ts) with session management + API key auth.
  • Adds unit tests for MCP handlers and HTTP auth helper; updates README and package scripts; adds @modelcontextprotocol/sdk dependency.

Reviewed changes

Copilot reviewed 8 out of 9 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
test/mcp-tools.test.ts Adds unit tests for MCP tool handler behavior and error serialization.
test/mcp-http-server.test.ts Adds unit tests for the isAuthorized helper.
src/mcp/tools.ts Implements MCP tool handler creation + tool registration with Zod schemas.
src/mcp/server.ts Adds stdio MCP server entrypoint wiring Prisma + tools/resources.
src/mcp/resources.ts Registers MCP resources for latest standards and rule lookup.
src/mcp/http-server.ts Adds Streamable HTTP MCP server with API key auth + session transport management.
README.md Documents MCP usage, scripts, environment variables, and client configuration.
package.json Adds MCP scripts and the @modelcontextprotocol/sdk dependency.
package-lock.json Locks new dependency tree for MCP SDK and transitive packages.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/mcp/http-server.ts
Comment thread src/mcp/http-server.ts Outdated
Comment thread src/mcp/http-server.ts Outdated
Comment thread src/mcp/http-server.ts
- Enforce 1 MiB body size cap in readBody; stream is destroyed on
  overflow (BodyTooLargeError) to avoid unbounded memory use
- Return 413 for oversized bodies and 400 for invalid JSON so clients
  get actionable errors instead of a 500
- Close McpServer in transport.onclose to prevent listener/resource
  leaks when sessions disconnect
- Call res.end() in serverError even when headers are already sent so
  SSE streams don't hang after a mid-stream error

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 9 changed files in this pull request and generated 4 comments.

Comment thread src/mcp/http-server.ts Outdated
Comment thread src/mcp/http-server.ts Outdated
Comment thread src/mcp/http-server.ts Outdated
Comment thread src/mcp/http-server.ts Outdated
- package.json / README.md: keep mcp:http:dev and mcp:http:start scripts
  and Streamable HTTP section added in this branch
- src/mcp/tools.ts: take main's version which includes statusCode: 500
  in the internal_error fallback (added in PR #2)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

Comment thread src/mcp/http-server.ts
Comment thread src/mcp/http-server.ts Outdated
Comment thread src/mcp/http-server.ts
lafronzt and others added 2 commits May 24, 2026 14:14
- Use JSON-RPC code -32600 (Invalid Request) in rejectBadRequest instead
  of -32000 (server error range) — helps clients classify client errors
  correctly
- Distinguish BadJsonError from I/O stream errors in handlePost body
  catch; unexpected stream errors now return 500 rather than 400
- Normalize process.argv[1] with path.resolve() before comparing to
  import.meta.url so the isMain guard works when npm passes a relative
  path (e.g. tsx src/mcp/http-server.ts from project root)
- Extract session management into exported createMcpHttpHandler factory
  so tests can inject a memory-backed service and inspect the sessions
  map without a real database
- Add 6 integration tests covering: 400 for non-init POST, GET, and
  DELETE without session IDs; session created and stored on initialize;
  subsequent POST routed to transport; 400 for unknown session ID on GET

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Reject empty POST bodies with 400 before session/transport routing to
  avoid forwarding undefined to transport.handleRequest, which would
  throw internally and produce a misleading 500
- Fix shutdown hang with open SSE sessions: close all McpServers in the
  session map, clear the map, then call httpServer.closeAllConnections()
  before awaiting httpServer.close() so the process exits promptly

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

Comment thread src/mcp/http-server.ts
Comment thread src/mcp/http-server.ts Outdated
- Return JSON-RPC shaped error bodies for oversized (-32600) and
  unparseable (-32700 Parse error) POST bodies so MCP/JSON-RPC clients
  can classify these failures correctly rather than receiving plain
  { error: '...' } objects
- Parse req.url with URL() and compare .pathname instead of the raw
  string so requests with query strings (e.g. /mcp?foo=bar) are routed
  correctly instead of getting a spurious 404

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.

Comment thread src/mcp/http-server.ts Outdated
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@lafronzt lafronzt merged commit b8c94dd into main May 24, 2026
1 check passed
@lafronzt lafronzt deleted the claude/relaxed-poincare-032335 branch May 24, 2026 18:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants